
# This is to be the system by which the resource manager will load archived
# resources. Currently it is incomplete and untested.

import os
from zipfile import ZipFile, error as ZipError, BadZipfile, LargeZipFile
from StringIO import StringIO
from error import Error,ArchdirError

_cache = {}

def fullsplit(path):
    """ Completely breaks down a file path into all its components with os.path.split.
        Returns a list. If the path ended with a directory separator (for
        example, '/' on POSIX) the final list item will be the empty string ''.
        FIXME: '/' at beginning of path is not present in returned list.
    """
    comps = []
    while path not in ('/',''):
        path,tail = os.path.split(path)
        comps.insert(0,tail)
    return comps

def open(path,mode='r',force=False):
    """ Attempts to open a file at the given path in the given mode.
        Archive files in the path are treated as directories.
        Internal instructions for opening a file are cached to make loading
        faster the next time the file is needed. If a set of cached instructions
        don't work (such as if the file was moved or deleted) then that path
        will be removed from the cache.
        Pass a true value to `force` to not use the cache.
    """
    # Use the cached instructions if they exist
    if not force and path in _cache:
        return _cache[path].get_file(mode)
    
    # Default values
    arch = None
    fObj = None
    archPath = None
    
    # Split the path into components
    comps = fullsplit(path)
    
    # Traverse the path.
    
    for n,comp in enumerate(comps):
        # If an archive is open,
        if arch is not None:
            # Try to open the path component as a file within the arch.
            try:
                # Get info about the file.
                archFileInfo = 
                pass # TODO: What to do if current path component is a file in arch
            # If path component could not be opened as file in archive,
            except KeyError:
                pass # TODO: What to do if current component is not a file in arch
        # If no archive is open,
        else:
            # Get the complete path to the current components
            fullPath = os.path.join(*comps[:n+1])
            # Try to open path component as an arch
            try:
                arch = ZipFile(fullPath,mode)
                iType = 1 # Instruction type 1: "Open as archive"
            # If path could not be opened as an arch,
            except (ZipError,BadZipfile,LargeZipFile,IOError):
                # Try to open path component as a file
                try:
                    fObj = open(fullPath)
                    iType = 0 # Instruction type 0: "Open as ordinary file"
                # If path could not be opened as a file,
                except IOError:
                    # and if the path is not a directory,
                    if not os.path.isdir(fullPath):
                        # Fail. The path can't be opened.
                        raise IOError("Try as I might, the path couldn't be opened in any way.")
                    # (Else) the component is a directory.
                    iType = -1 # fake instruction type; no DirectAccessor cached
       
        # Caching instructions.
        
        # If the path component was to a directory,
        if iType == -1:
            # Cache null instruction; nothing is done for directories.
            _cache[fullPath] = None
            # FIXME: Is fullPath always the correct path for this scenario?
        # If the path component was an ordinary file,
        elif iType == 0:
            # Cache the instructions for opening the file as-is
            _cache[fullPath] = DirectAccessor((fullPath,0))
            continue
        # If the path component is to an archive file,
        elif iType == 1:
            # Cache instructions for opening the file as an archive
            _cache[fullPath] = DirectAccessor((fullPath,1))
        # If the path component was to an ordinary file within an archive,
        elif iType == 2:
            pass # TODO: iType == 2
        # If the path component was to an archive within an archive,
        elif iType == 3:
            pass # TODO: iType == 3
        # If an unknown instruction was found,
        else:
            raise ArchdirError("Internal error; encountered bad iType: %s"%iType)
    else:
        # Should never end on any iType besides 0 or 2
        if iType not in (0,2):
            raise ArchdirError("Ended path traversal on iType %s"%iType)
    
    # Clean up.
    if arch is not None:
        arch.close()
    
    return fObj

class DirectAccessor(object):
    """ A DirectAccessor is used to get direct access to one particular file
        given a series of paths on the filesystem and within archive files.
        This is used with caching.
    """
    def __init__(self,*args):
        # Example args:
        # Open 'resource/image/surface':
        # >>> args = (('resource/image/surface.bmp',0))
        # Open 'resources/image/image.zip/surface.bmp':
        # >>> args = (('resource/image/image.zip',1),('surface.bmp',2))
        # Open 'a/b/c.zip/e/f.zip/g.txt'
        # >>> args = (('a/b/c.zip',1'),('e/f.zip',3),('g.txt',2))
        self._instruction = args
    
    def get_file(self,mode='r'):
        lastPath,lastType = None,None
        arch = None
        # Parse the instructions
        for path,type in self._instruction:
            if type == 0: # Ordinary file within directory. Terminal.
                return file(path,mode)
            elif type == 1: # Archive-as-directory. Non-terminal.
                arch = ZipFile(path,mode)
            elif type == 2: # Ordinary file in arch-as-dir. Terminal.
                fObj = StringIO(arch.read(path))
                arch.close()
                return fObj
                # FIXME: Doesn't allow for saving back to ZipFile
            elif type == 3: # Arch-as-dir in arch-as-dir. Non-terminal.
                fObj = StringIO(arch.read(path))
                newarch = ZipFile(fObj, mode)
                fObj.close()
                arch.close()
                arch = newarch
                # FIXME: Makes save back to zip file utterly impossible
            else:
                raise ArchdirError("Bad instruction type encountered: %s"%type)
        else:
            if arch is not None:
                arch.close()
            raise ArchdirError("Incomplete instruction set.")
